// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Author: Markus Gutschke

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "check.h"
#include "ieee1284.h"
#include "ijs.h"
#include "ijs_backend.h"
#include "output_print_job.h"
#include "stringprintf.h"

#define min(a,b) ({ typeof(a) _a=(a); typeof(b) _b=(b); _a < _b ? _a : _b; })
#define max(a,b) ({ typeof(a) _a=(a); typeof(b) _b=(b); _a > _b ? _a : _b; })


struct IJSPrinterBackend {
  struct PrinterBackend fnc;
  const char *ijsServer;
  const char *dev;
  const char *model;
};

struct State {
  struct PrinterBackend *backend;
  struct PrintParams    params;
  int                   out;
  struct IJS            *ijs;
  uint32_t              job;
  int                   top, left, width, height;
  int                   skipTop, skipLeft, bytesPerLine, skipRight, numLines;
  int                   skipping, lineCount;
  enum { ST_TOP, ST_LEFT, ST_LINE, ST_RIGHT } pos;
};

static void writePrinterData(struct IJS *ijs, const char *buf, size_t sz,
                             void *arg) {
  struct State *state = (struct State *)arg;
  check(TEMP_FAILURE_RETRY(write(state->out, buf, sz)) == sz);
}

static void setManufacturerAndModel(struct IJSPrinterBackend *ijsBackend,
                                    struct State *state) {
  if (ijsBackend->model) {
    // If we were already told the model of the printer, that's exactly
    // what we will be using.
    const char *model = strchr(ijsBackend->model, ';');
    if (model) {
      // If the model string included a semicolon, then everything before that
      // is the name of the manufacturer. Many printer drivers don't actually
      // need this. So, continue without setting the manufacturer, if we don't
      // know it.
      check(!ijsSetParam(state->ijs, state->job, "DeviceManufacturer",
                         ijsBackend->model, model - ijsBackend->model));
      ++model;
    } else {
      model = ijsBackend->model;
    }
    check(!ijsSetParam(state->ijs, state->job, "DeviceModel", model, -1));
  } else {
    const char *ieee1284 = ieee1284Id(ijsBackend->dev);

    // If we can find a manufacturer name, provide it to the printer driver.
    char *ptr = strstr(ieee1284, "MANUFACTURER:");
    if (ptr) {
      ptr += 9;
    } else {
      ptr = strstr(ieee1284, "MFG:");
    }
    if (ptr) {
      ptr += 4;
      if (ijsSetParam(state->ijs, state->job, "DeviceManufacturer",
                      ptr, strcspn(ptr, ";"))) {
        // A couple of device manufacturers have historically used different
        // spellings of their name. Try all the known combinations.
        char ***alt = (char**[]){
          (char*[]){ "Epson", "EPSON", 0 },
          (char*[]){ "Hewlett-Packard", "HEWLETT-PACKARD", "hp", "HP", 0 },
          (char*[]){ "Kyocera", "Kyocera Mita", 0 },
          (char*[]){ "Lexmark", "Lexmark International", 0 },
          (char*[]){ "Minolta", "MINOLTA", 0 },
          (char*[]){ "Xerox", "XEROX", 0 }
        };
        for (int i = 0; i < sizeof(alt)/sizeof(*alt); ++i) {
          for (char **name = alt[i]; *name; ++name) {
            if (!memcmp(*name, ptr, strlen(*name))) {
              name = alt[i];
              for (; *name; ++name) {
                if (!ijsSetParam(state->ijs, state->job, "DeviceManufacturer",
                                 *name, -1)) {
                  break;
                }
              }
              i = sizeof(alt)/sizeof(*alt);
            }
          }
        }
        // If we still don't have match for the device manufacturer, try
        // continuing without setting it. For many drivers, that happens to
        // work OK.
      }
    }

    // Now do the same thing for the model name. Unlike the manufacturer name,
    // the model is mandatory.
    ptr = strstr(ieee1284, "MODEL:");
    if (ptr) {
      ptr += 2;
    } else {
      ptr = strstr(ieee1284, "MDL:");
    }
    check(ptr);
    ptr += 4;
    int mdlLen = strcspn(ptr, ";");
    if (ijsSetParam(state->ijs, state->job, "DeviceModel", ptr, mdlLen)) {
      // Now it gets complicated. The IJS specification recommends that drivers
      // recognize the model name from the IEEE1284 string. In practice, they
      // often don't do this and instead use a driver-specific internal
      // database.
      int modelSetSuccessfully = 0;

      // For the Gutenprint driver, look in
      // /usr/share/gutenprint/*/xml/printers.xml and see if we can find a
      // match on either the MODEL in the IEEE1284 string (not all entries
      // in printers.xml have a IEEE1284 string), or on the printer name (all
      // entries do have a printer name, but it doesn't always match with what
      // the printer reports in its IEEE1284 string).
      // The printers.xml file happens to have one printer record per line.
      // That simplifies our code significantly and we don't actually need to
      // write a full XML parser.
      static const char gutenprint[] = "/usr/share/gutenprint";
      DIR *dir = opendir(gutenprint);
      if (dir) {
        char dirbuf[offsetof(struct dirent, d_name) + PATH_MAX + 1];
        struct dirent *entry;
        while (!modelSetSuccessfully &&
               !readdir_r(dir, (struct dirent *)dirbuf, &entry) && entry) {
          if (*entry->d_name == '.') {
            continue;
          }
          char *printers = stringPrintf(NULL, "%s/%s/xml/printers.xml",
                                        gutenprint, entry->d_name);
          FILE *fp = fopen(printers, "r");
          free(printers);
          if (fp) {
            // Found a printers.xml file on this system.
            char line[256];
            while (!modelSetSuccessfully &&
                   fgets(line, sizeof(line), fp)) {
              char *mdl = strstr(line, "MODEL:");
              if (mdl) {
                mdl += 2;
              } else {
                mdl = strstr(line, "MDL:");
              }
              if (mdl && mdlLen == strcspn(mdl + 4, ";\"") &&
                  !memcmp(mdl + 4, ptr, mdlLen)) {
                // Found a printer description that included an IEEE1284
                // string. And that string's MODEL matches the MODEL reported
                // by our printer.
                goto found_model;
              } else {
                mdl = strstr(line, "name=\"");
                if (mdl && mdlLen == strcspn(mdl + 6, "\"") &&
                    !memcmp(mdl + 6, ptr, mdlLen)) {
                  // While the printer description did not include an IEEE1284
                  // string, it's printer name matches the MODEL reported by
                  // our printer.
                found_model:;
                  char *driver = strstr(line, "driver=\"");
                  modelSetSuccessfully |=
                    driver &&
                    !ijsSetParam(state->ijs, state->job, "DeviceModel",
                                 driver + 8, strcspn(driver + 8, "\""));
                }
              }
            }
            fclose(fp);
          }
        }
        closedir(dir);
      }

      // For hpijs, we apply a couple of simple heuristics. This won't cover
      // everything. But it works in many common casses.
      if (!modelSetSuccessfully) {
        char *mdl;
        check(mdl = malloc(mdlLen + 1));
        for (int i = 0; i < mdlLen; ++i, ++ptr) {
          mdl[i] = *ptr >= 'A' && *ptr <= 'Z' ? *ptr | 0x20 : *ptr;
        }
        mdl[mdlLen] = '\000';
        if (strstr(mdl, "laserjet")) {
          // If the printer's MODEL contains the substring LaserJet with any
          // arbitrary capitalization, then use a generic LaserJet driver.
          if (strstr(mdl, "color")) {
            // We distinguish between color and monochrome LaserJets
            modelSetSuccessfully |=
              !ijsSetParam(state->ijs, state->job, "DeviceModel",
                           "ColorLaser", -1);
          }
          if (!modelSetSuccessfully) {
            modelSetSuccessfully |=
              !ijsSetParam(state->ijs, state->job, "DeviceModel",
                           "Mono Laser", -1);
          }
        } else if (strstr(ieee1284, "DESKJET")) {
          // This is really a last-ditch effort. If we see the string
          // DESKJET (probably either in MDL or CMD), pick a generic DeskJet
          // driver. That might or might not work correctly, as DeskJet
          // printers have a large amount of variability.
          modelSetSuccessfully |=
            !ijsSetParam(state->ijs, state->job, "DeviceModel",
                         "GenericVIP", -1);
        }
      }

      check(modelSetSuccessfully);
    }
    free((void *)ieee1284);
  }
}

static void *startJob(struct PrinterBackend *backend,
                      const struct PrintParams *params) {
  struct IJSPrinterBackend *ijsBackend = (struct IJSPrinterBackend *)backend;
  struct State *state;
  check(state = malloc(sizeof(struct State)));
  state->backend = backend;
  state->params  = *params;

  check((state->out = open(ijsBackend->dev,
                           O_CREAT|O_TRUNC|O_WRONLY, 0777)) >= 0);
  state->ijs = newIJS(ijsBackend->ijsServer, writePrinterData, state);
  ijsOpen(state->ijs);
  state->job = ijsBeginJob(state->ijs);
  setManufacturerAndModel(ijsBackend, state);

  if (ijsBackend->fnc.newParams(state, params)) {
    check(!TEMP_FAILURE_RETRY(close(state->out)));
    ijsEndJob(state->ijs, state->job);
    ijsClose(state->ijs);
    deleteIJS(state->ijs);
    free(state);
    return NULL;
  }
  return state;
}

static int newParams(void *arg, const struct PrintParams *params) {
  struct State *state = (struct State *)arg;
  state->params = *params;
    
  check(!ijsSetParamF(state->ijs, state->job, "NumChan", "%d",
                      params->numColors));
  check(!ijsSetParamF(state->ijs, state->job, "BitsPerSample", "%d",
                      params->bitsPerColor));
  if (ijsSetParam(state->ijs, state->job, "ColorSpace", "DeviceRGB", -1)) {
    check(!ijsSetParam(state->ijs, state->job, "ColorSpace", "sRGB", -1));
  }
  const char *val;
  check(val = ijsGetParam(state->ijs, NULL, state->job, "PrintableTopLeft"));
  check(!ijsSetParam(state->ijs, state->job, "TopLeft", val, -1));
  double tInch, lInch, wInch, hInch;
  check(sscanf(val, "%lfx%lf", &tInch, &lInch) == 2);
  check(val = ijsGetParam(state->ijs, NULL, state->job, "PrintableArea"));
  check(sscanf(val, "%lfx%lf", &wInch, &hInch) == 2);
  int t = ceil(tInch * params->vRes);
  int l = ceil(lInch * params->hRes);
  if (params->bitsPerPixel == 1) {
    l = (l + 7) & -8;
  }
  int w, h;
  if (wInch) {
    w                 = floor(wInch * params->hRes);
  } else {
    // If the printer driver doesn't know the printable size, make an
    // educated guess.
    w                 = params->width - 2*l;
  }
  if (hInch) {
    h                 = floor(hInch * params->vRes);
  } else {
    // If the printer driver doesn't know the printable size, make an
    // educated guess.
    h                 = params->height - 2*t;
  }
  state->top          = max(0, t);
  state->left         = max(0, l);
  state->width        = max(0, min(w, params->width - l));
  state->height       = max(0, min(h, params->height - t));
  state->skipTop      = t * params->bytesPerLine;
  state->skipLeft     = l * params->bitsPerPixel/8;
  state->bytesPerLine = max(0, (state->width*params->bitsPerPixel+7)/8);
  state->skipRight    = (params->bytesPerLine - state->bytesPerLine) -
                        state->skipLeft;
  state->numLines     = max(0, state->height);
  state->pos          = ST_TOP;
  if (state->width > 0 && state->height > 0) {
    check(!ijsSetParamF(state->ijs, state->job, "Dpi", "%dx%d",
                        params->hRes, params->vRes));
    check(!ijsSetParamF(state->ijs, state->job, "Width", "%d", state->width));
    check(!ijsSetParamF(state->ijs, state->job, "Height","%d", state->height));
  }
  return 0;
}

static int  startPage(void *arg) {
  struct State *state = (struct State *)arg;
  if (state->width > 0 && state->height > 0) {
    ijsBeginPage(state->ijs, state->job);
    state->pos       = ST_TOP;
    state->skipping  = state->skipTop;
    state->lineCount = 0;
  }
  return 0;
}

static int  sendData(void *arg, const char *data, size_t sz) {
  // The CloudPrint server provides us with a raster image that covers the
  // entire page. Quite frequently, printers cannot print all the way to the
  // edge. When computing parameters in newParams() we figured out how
  // many bytes we need to trim on all sides. We now take this information
  // into account before forwarding pixel data to the IJS driver.
  struct State *state = (struct State *)arg;
  while (sz > 0 && state->lineCount < state->numLines) {
    switch (state->pos) {
    case ST_TOP:;
      size_t bytes      = max(0, min(sz, state->skipping));
      state->skipping  -= bytes;
      sz               -= bytes;
      data             += bytes;
      if (state->skipping) {
        break;
      }
      state->skipping   = state->skipLeft;
      state->pos        = ST_LEFT;
      // fall thru
    case ST_LEFT:
      bytes             = max(0, min(sz, state->skipping));
      state->skipping  -= bytes;
      sz               -= bytes;
      data             += bytes;
      if (state->skipping) {
        break;
      }
      state->skipping   = state->bytesPerLine;
      state->pos        = ST_LINE;
      // fall thru
    case ST_LINE:
      bytes             = max(0, min(sz, state->skipping));
      if (bytes) {
        ijsSendData(state->ijs, state->job, data, bytes);
      }
      state->skipping  -= bytes;
      sz               -= bytes;
      data             += bytes;
      if (state->skipping) {
        break;
      }
      state->skipping   = state->skipRight;
      state->pos        = ST_RIGHT;
      // fall thru
    case ST_RIGHT:
      bytes             = max(0, min(sz, state->skipping));
      state->skipping  -= bytes;
      sz               -= bytes;
      data             += bytes;
      if (!state->skipping) {
        ++state->lineCount;
        state->skipping = state->skipLeft;
        state->pos      = ST_LEFT;
      }
      break;
    }
  }
  return 0;
}

static int finishPage(void *arg) {
  struct State *state = (struct State *)arg;
  check(state->lineCount >= state->numLines);
  if (state->width > 0 && state->height > 0) {
    ijsEndPage(state->ijs, state->job);
  }
  return 0;
}

static int finishJob(void *arg) {
  struct State *state = (struct State *)arg;
  ijsEndJob(state->ijs, state->job);
  ijsClose(state->ijs);
  deleteIJS(state->ijs);
  check(!TEMP_FAILURE_RETRY(close(state->out)));
  free(arg);
  return 0;
}

static void destroyIJSPrinterBackend(struct PrinterBackend *backend) {
  struct IJSPrinterBackend *ijsBackend = (struct IJSPrinterBackend *)backend;
  if (ijsBackend) {
    free((void *)ijsBackend->ijsServer);
    free((void *)ijsBackend->dev);
    free((void *)ijsBackend->model);
  }
}

struct PrinterBackend *newIJSPrinterBackend(const char *ijsServer,
                                            const char *dev,
                                            const char *model) {
  struct IJSPrinterBackend *ijsBackend;
  check(ijsBackend = calloc(1, sizeof(struct IJSPrinterBackend)));
  memcpy(&ijsBackend->fnc, &(struct PrinterBackend) {
      .destructor = destroyIJSPrinterBackend,
      .startJob   = startJob,
      .newParams  = newParams,
      .startPage  = startPage,
      .sendData   = sendData,
      .finishPage = finishPage,
      .finishJob  = finishJob,
    }, sizeof(struct PrinterBackend));
  check(ijsBackend->ijsServer = strdup(ijsServer));
  check(ijsBackend->dev = strdup(dev));
  if (model) {
    check(ijsBackend->model = strdup(model));
  }
  return (struct PrinterBackend *)ijsBackend;
}
